/*
 *
 *  *    Copyright 2020-2021 Luter.me
 *  *
 *  *    Licensed under the Apache License, Version 2.0 (the "License");
 *  *    you may not use this file except in compliance with the License.
 *  *    You may obtain a copy of the License at
 *  *
 *  *      http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *    Unless required by applicable law or agreed to in writing, software
 *  *    distributed under the License is distributed on an "AS IS" BASIS,
 *  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *    See the License for the specific language governing permissions and
 *  *    limitations under the License.
 *
 */

package com.luter.heimdall.core.token.store;

 
import com.luter.heimdall.core.authorization.store.AuthorizationStore;
import com.luter.heimdall.core.cache.MapMemoryCache;
import com.luter.heimdall.core.cache.MemoryCache;
import com.luter.heimdall.core.config.ConfigManager;
import com.luter.heimdall.core.config.HeimdallProperties;
import com.luter.heimdall.core.details.UserDetails;
import com.luter.heimdall.core.exception.HeimdallCacheException;
import com.luter.heimdall.core.exception.HeimdallExceededTokenException;
import com.luter.heimdall.core.exception.HeimdallTokenException;
import com.luter.heimdall.core.listener.AbstractTokenStoreEvent;
import com.luter.heimdall.core.token.PageModel;
import com.luter.heimdall.core.token.SimpleToken;
import com.luter.heimdall.core.utils.StrUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.slf4j.Logger;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static org.slf4j.LoggerFactory.getLogger;

/**
 * Token Store  内存缓存实现
 * <p>
 * 1、配置WebContextHolder，可实现：Cookie 功能、获取客户端 IP 功能。
 * <p>
 * 2、配置AuthorizationStore，在 token 作废的时候，将同步清理用户权限缓存数据
 *
 * @author luter
 */
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true, fluent = true)
public class MemoryCachedTokenStore extends AbstractTokenStoreEvent implements TokenStore {
    /**
     * The constant log.
     */
    private static final transient Logger log = getLogger(MemoryCachedTokenStore.class);
    /**
     * Token 缓存
     */
    private MemoryCache<String, SimpleToken> tokenCache;

    /**
     * Token 删除后，同步删除用户的权限缓存，权限缓存也会被定时清理任务清理
     */
    private AuthorizationStore authorizationStore;

    /**
     * Instantiates a new Memory cached token store.
     */
    public MemoryCachedTokenStore() {
        this.tokenCache = new MapMemoryCache<>(new ConcurrentHashMap<>(), "DEFAULT_MAP_TOKEN_STORE");
    }

    /**
     * Instantiates a new Memory cached token store.
     *
     * @param tokenCache the token cache
     */
    public MemoryCachedTokenStore(MemoryCache<String, SimpleToken> tokenCache) {
        this();
        this.tokenCache = tokenCache;
    }

    /**
     * Instantiates a new Memory cached token store.
     *
     * @param tokenCache         the token cache
     * @param authorizationStore the authorization store
     */
    public MemoryCachedTokenStore(MemoryCache<String, SimpleToken> tokenCache,
                                  AuthorizationStore authorizationStore) {
        this();
        this.tokenCache = tokenCache;
        this.authorizationStore = authorizationStore;
    }

    @Override
    public boolean isSelfExpired() {
        return false;
    }

    @Override
    public SimpleToken saveToken(SimpleToken token) {
        log.debug("[saveToken]::token = [{}]", token);
        return cacheToken(token);
    }

    /**
     * 缓存 token,按给定时长产生 cookie
     *
     * @param token Token
     * @return the simple token
     */
    protected SimpleToken cacheToken(SimpleToken token) {
        resolveMaxTokens(token.getDetails());
        HeimdallProperties config = ConfigManager.getConfig();
        log.debug("[cacheToken]::token = [{}]", token);
        //放入缓存
        final SimpleToken putToken = tokenCache.put(generateCacheKey(token.getId()), token);
        //允许多次登录，且配置了权限缓存 Dao,则清理上次用户的缓存权限
        if (config.getToken().getMaximumTokens() > 1 && null != authorizationStore) {
            authorizationStore.removeUserAuthorities(putToken.getDetails());
        }
        onCreated(putToken);
        return putToken;
    }

    @Override
    public SimpleToken getToken(String tokenId) {
        log.debug("[getToken]::tokenId = [{}]", tokenId);
        if (StrUtils.isBlank(tokenId)) {
            return null;
        }
        final SimpleToken simpleToken = tokenCache.get(generateCacheKey(tokenId));
        log.debug("[getToken]::tokenId = [{}],token = [{}]", tokenId, simpleToken);
        onRead(simpleToken);
        return simpleToken;
    }

    @Override
    public SimpleToken update(SimpleToken token) {
        HeimdallProperties config = ConfigManager.getConfig();
        log.debug("[update]::token = [{}]", token);
        //开启了 token 自动续期
        if (config.getToken().isRenewal()) {
            ////内存缓存,自身 exp 延长后，重新写入缓存即可
            final SimpleToken newToken = token.renewalToken(token, config.getToken().getTimeout());
            //写入缓存,直接 Put覆盖
            tokenCache.put(generateCacheKey(token.getId()), newToken);
            log.debug("[update]::token = [{}],newToken = [{}]", token, newToken);
            //发布事件
            onUpdated(newToken);
            return newToken;
        }
        //否则 啥也不干
        return token;
    }

    @Override
    public Collection<SimpleToken> getPrincipalTokens(String principal) {
        log.debug("[getPrincipalTokens]::principal = [{}]", principal);
        if (StrUtils.isBlank(principal)) {
            return new ArrayList<>();
        }
        Collection<SimpleToken> tokens = new ArrayList<>();
        final Collection<SimpleToken> activeTokens = getActiveTokens();
        for (SimpleToken activeToken : activeTokens) {
            if (principal.equals(activeToken.getDetails().getPrincipal())) {
                tokens.add(activeToken);
            }
        }
        log.debug("[getPrincipalTokens]::principal = [{}],tokens = [{}]", principal, tokens);
        return tokens;
    }

    @Override
    public Collection<SimpleToken> getActiveTokens() {
        log.debug("[getActiveTokens]::");
        final Collection<SimpleToken> values = tokenCache.values();
        final List<SimpleToken> simpleTokens = values.stream()
                .sorted(Comparator.comparing(SimpleToken::getIat).reversed())
                .filter(v -> !v.hasExpired()).collect(Collectors.toList());
        log.debug("[getActiveTokens]::count = [{}]", simpleTokens.size());
        return simpleTokens;
    }

    @Override
    public Collection<SimpleToken> getActiveTokens(String appId) {
        log.debug("[getActiveTokens]::appId = [{}]", appId);
        if (StrUtils.isNotBlank(appId)) {
            final Collection<SimpleToken> activeTokens = getActiveTokens();
            final List<SimpleToken> simpleTokens = activeTokens.stream()
                    .sorted(Comparator.comparing(SimpleToken::getIat).reversed())
                    .filter(d -> d.getDetails().getAppId()
                            .equals(appId)).collect(Collectors.toList());
            log.debug("[getActiveTokens]::appId = [{}],count = [{}]", appId, simpleTokens.size());
            return simpleTokens;
        }
        throw new HeimdallCacheException("Illegal Argument,appId can not be null.");
    }

    @Override
    public SimpleToken deleteToken(String tokenId) {
        HeimdallProperties config = ConfigManager.getConfig();
        log.debug("[deleteToken]::tokenId = [{}]", tokenId);
        if (StrUtils.isBlank(tokenId)) {
            throw new HeimdallCacheException("tokenId can not be null");
        }
        final SimpleToken remove = tokenCache.remove(config.getToken().getCachePrefix() + tokenId);
        log.debug("[deleteToken]::tokenId = [{}], remove token = [{}]", tokenId, remove);
        onDeleted(remove);
        //给缓存清理掉
        if (null != authorizationStore) {
            log.debug("[deleteToken]:: clear the UserAuthorities at the same time after remove token, tokenId = [{}]", tokenId);
            authorizationStore.removeUserAuthorities(remove.getDetails());
        }
        return remove;
    }

    @Override
    public int deletePrincipalTokens(String principal) {
        HeimdallProperties config = ConfigManager.getConfig();
        int count = 0;
        log.debug("[deletePrincipalTokens]::principal = [{}]", principal);
        if (StrUtils.isNotBlank(principal)) {
            final Collection<SimpleToken> activeTokens = tokenCache.values();
            for (SimpleToken token : activeTokens) {
                if (principal.equals(token.getDetails().getPrincipal())) {
                    tokenCache.remove(config.getToken().getCachePrefix() + token.getId());
                    //给缓存清理掉
                    if (null != authorizationStore) {
                        authorizationStore.removeUserAuthorities(token.getDetails());
                    }
                    count++;
                }
            }
        }
        log.debug("[deletePrincipalTokens]::principal = [{}],count = [{}]", principal, count);
        return count;
    }

    @Override
    public int deleteAppTokens(String appId) {
        int count = 0;
        log.debug("[deleteAppTokens]::appId = [{}]", appId);
        if (StrUtils.isNotBlank(appId)) {
            final Collection<SimpleToken> tokens = getActiveTokens();
            for (SimpleToken token : tokens) {
                final String theId = token.getDetails().getAppId();
                if (theId.equals(appId)) {
                    tokenCache.remove(generateCacheKey(token.getId()));
                    //把用户权限缓存也清理掉
                    if (null != authorizationStore) {
                        authorizationStore.removeUserAuthorities(token.getDetails());
                    }
                    count++;
                }
            }
        }
        log.debug("[deleteAppTokens]::appId = [{}],count = [{}]", appId, count);
        return count;
    }

    @Override
    public int clearExpiredTokens() {
        int count = 0;
        log.debug("[clearExpiredTokens]::Start invalid token cleanup task now");
        final Set<String> keys = tokenCache.keys();
        for (String key : keys) {
            final SimpleToken token = tokenCache.get(key);
            if (null != token && token.hasExpired()) {
                tokenCache.remove(key);
                //给缓存清理掉
                if (null != authorizationStore) {
                    authorizationStore.removeUserAuthorities(token.getDetails());
                }
                count++;
                log.debug("[clearExpiredTokens]::Expired token with key: [ {} ] has been cleared", key);
            }
        }
        if (count > 0) {
            log.info("[clearExpiredTokens]::The invalid token cleaning task ended, and [ {} ] tokens were cleaned up", count);
        }

        log.debug("[clearExpiredTokens]::count = [{}]", count);
        return count;
    }

    @Override
    public long getActiveTokensCount() {
        return tokenCache.size();
    }

    @Override
    public PageModel<SimpleToken> getActiveTokensPage(int pageNumber, int pageSize) {
        final Collection<SimpleToken> activeTokens = getActiveTokens();
        if (null == activeTokens || activeTokens.isEmpty()) {
            return new PageModel<>();
        }
        return new PageModel<>(activeTokens, pageNumber, pageSize);
    }

    @Override
    public PageModel<SimpleToken> getActiveTokensPage(String appId, int pageNumber, int pageSize) {
        final Collection<SimpleToken> activeTokens = getActiveTokens(appId);
        if (null == activeTokens || activeTokens.isEmpty()) {
            return new PageModel<>();
        }
        return new PageModel<>(activeTokens, pageNumber, pageSize);
    }

    /**
     * 产生 token 缓存 key
     *
     * @param id TokenId
     * @return the string
     */
    private String generateCacheKey(String id) {
        HeimdallProperties config = ConfigManager.getConfig();
        log.debug("[generateCacheKey]::id = [{}]", id);
        return generateCacheKey(config.getToken().getCachePrefix(), id);
    }

    /**
     * 构造包含前缀和 唯一凭据 的缓存 key
     *
     * @param orgPrefix 设置的前缀
     * @param id        缓存的唯一 ID
     * @return 最终缓存 key
     */
    private String generateCacheKey(String orgPrefix, String id) {
        if (StrUtils.isBlank(id)) {
            throw new HeimdallCacheException("id can not be null");
        }
        if (StrUtils.isBlank(orgPrefix)) {
            throw new HeimdallCacheException("prefix can not be null");
        }

        log.debug("[generateCacheKey]::orgPrefix = [{}], id = [{}]", orgPrefix, id);
        String prefix = orgPrefix.trim();
        prefix = prefix.startsWith(StrUtils.COLON) ? prefix.replaceFirst(StrUtils.COLON, StrUtils.EMPTY_STRING) : prefix;
        return prefix.endsWith(StrUtils.COLON) ? prefix + id : prefix + StrUtils.COLON + id;
    }

    /**
     * 并发登录处理逻辑
     *
     * @param userDetails the user details
     */
    private void resolveMaxTokens(UserDetails userDetails) {
        HeimdallProperties config = ConfigManager.getConfig();
        final int maximumTokens = config.getToken().getMaximumTokens();
        log.debug("[resolveMaxTokens]::userDetails = [{}]", userDetails);
        //小于-1 的，提示一下，就当 -1 对待了
        if (maximumTokens < 0) {
//            throw new IllegalArgumentException("maximum tokens can not be less than -1");
            log.warn("[resolveMaxTokens]::Bad parameter settings .maximum tokens is set to {}, " +
                    "The concurrent authentication restriction function will disabled。", maximumTokens);
        }
        //拒绝登录了
        if (0 == maximumTokens) {
            log.warn("[resolveMaxTokens]::maximum tokens is set to {}, The  authentication  " +
                    "function will disabled。", maximumTokens);
            throw new HeimdallTokenException("authentication has been disabled");

        }
        //开启了重复登录限制
        if (maximumTokens > 0) {
            final Collection<SimpleToken> principalTokens = getPrincipalTokens(userDetails.getPrincipal());
            //超过次数了
            if (null != principalTokens && principalTokens.size() >= maximumTokens) {
                if (config.getToken().isMaxTokensPreventLogin()) {
                    log.warn("[resolveMaxTokens]::userDetails:[{}],   exceeded the maximum Tokens limit of [{}] times ." +
                            " request rejected", userDetails, maximumTokens);
                    throw new HeimdallExceededTokenException();
                } else {
                    //把前面的都踢出去

                    final int deletePrincipalTokens = deletePrincipalTokens(userDetails.getPrincipal());
                    log.warn("[resolveMaxTokens]:: Remove all previously authenticated tokens of the User:  " +
                            "userDetails = [{}],count = [{}]", userDetails, deletePrincipalTokens);
                }

            }
        }
    }

}
